ACTIVITY 03

Persisting Domain Objects

Map a DDD domain model to a relational database using Spring Data JPA, Hibernate, H2, and Lombok.

60 minutes #DDD #JPA #Hibernate #Lombok #Persistence #SpringData

Overview

In Activity 02, you designed the domain model of a Library Management System using Domain-Driven Design (DDD). You identified entities, value objects, aggregate roots, bounded contexts, and relationships between domain concepts.

In this activity, we take the next step: we persist the domain model.

Important: You are not going to write every class from scratch. Most of the implementation is provided for you. Your job is to put the code in the correct location, understand the important parts, run the application, and verify that Hibernate creates the database schema.

We will deliberately postpone inserting application data. Data initialization, CRUD operations, and repository usage will be covered in Activity 04.

Learning Goals

By the end of this activity, you should be able to:

  • Explain the relationship between a DDD domain model and a relational database.
  • Map a domain entity to a JPA entity using @Entity.
  • Identify an entity's database identity using @Id.
  • Map Value Objects using @Embeddable and @Embedded.
  • Map Java enumerations using @Enumerated(EnumType.STRING).
  • Explain why Lombok can reduce boilerplate code.
  • Explain why @Data should not automatically be used on JPA entities.
  • Understand the role of Spring Data JPA repositories.
  • Verify that Hibernate has created database tables using the H2 Console.

Before You Begin

You should already have:

  • Completed Activity 02.
  • The Library Management System Spring Boot project.
  • Java and Gradle configured correctly.
  • Spring Boot running successfully.
Do not create database data yet. In this activity we are interested in the database structure, not the data.

Deliverables

Before submitting the activity, make sure your project contains:

  • The three bounded-context package structures.
  • Member Management JPA entities and Value Objects.
  • Spring Data JPA repositories.
  • Appropriate Lombok annotations.
  • A successfully running Spring Boot application.
  • Database tables visible in the H2 Console.
No database data is required for Activity 03. Do not spend time creating sample members, loans, or books. We will do this in Activity 04.

1. From Domain Model to Database

A DDD model describes the business concepts of an application. A relational database stores data in tables, rows, and columns.

JPA provides a way to connect these two worlds. Hibernate is the JPA implementation that will perform the actual object-relational mapping.

 

DDD Domain Model
|
|  JPA annotations
v
Java Objects
|
|  Hibernate
v
Relational Database
|
v
H2 Tables 

The important idea is that JPA maps the objects; it does not design your domain model for you.

Key principle: First design the domain model. Then decide how that model should be persisted.

2. Organizing the Project by Bounded Context

In Activity 02, we separated the Library Management System into different bounded contexts. We will continue this organization in the Java project. Each subdomain gets its own package, and each package contains separate layers.

Thus your first task is to create the necessary packages and structure for each bounded context. You can do that inside an IED like IntelliJ or Eclipse, or you can create the packages manually in the file system. i.e. using the file explorer or command line. The important thing is to create the packages in the correct location, and with the correct names.

Project Package Structure

                                       src/main/java/com/example/library
                 ____________________________________________________________________________________             
                |                                       |                                            |
                     
membermanagement
│
├── domainlayer
│   ├── entity
│   ├── valueobject
│   └── 
│
├── infrastructurelayer
│   └── repository
│
├── businesslogiclayer
│   └── services
│
└── presentationlayer
└── controllers
 
loanmanagement
│
├── domainlayer
│   ├── entity
│   ├── valueobject
│   └── 
│
├── infrastructurelayer
│   └── repository
│
├── businesslogiclayer
│   └── services
│
└── presentationlayer
└── controllers
   
collectionmanagement
│
├── domainlayer
│   ├── entity
│   ├── valueobject
│   └── repository
│
├── infrastructurelayer
│   └── repository
│
├── businesslogiclayer
│   └── services
│
└── presentationlayer
└── controllers
Architecture rule: The domain layer contains the business model. Infrastructure contains technology-specific code such as JPA and Spring Data.
Use the file explorer: use the file explorer from your Operating System to create the packages and sub-packages. It is way easier than trying to do it from the IDE.

Part 1 : Reducing Boilerplate with Lombok

Java classes often contain a large amount of repetitive code: getters, setters, constructors, toString(), equals(), and hashCode().

Lombok is a library that can generates much of this code automatically during compilation, so you don't have to write it yourself. The following example shows a simple class with and without Lombok, so you can see the difference.

Without Lombok

public class Member {
    private String name;
    private String email;

    public Member() {
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
}

With Lombok

@Getter
@Setter
@NoArgsConstructor
public class Member {
    private String name;
    private String email;
}

You can see that the first version is longer and contains a lot of boilerplate code. The second version is shorter and easier to read. It also saves you time and reduces the risk of errors. Let us understand the meaning of the annotations used in the second version of the class.

Important Lombok Annotations

Annotation Purpose
@Getter Generates getter methods.
@Setter Generates setter methods.
@NoArgsConstructor Generates a no-argument constructor.
@RequiredArgsConstructor Generates a constructor for required fields.
@AllArgsConstructor Generates a constructor containing all fields.
@Builder Provides the Builder pattern.
@ToString Generates toString().
@EqualsAndHashCode Generates equality methods.
@Data Combines several Lombok features.

All these annotations are optional. You can use only the ones you need. For example, if you only need getters and setters, you can use only @Getter and @Setter. If you need a no-argument constructor, you can use @NoArgsConstructor. If you need a constructor with required fields, you can use @RequiredArgsConstructor. If you need a constructor with all fields, you can use @AllArgsConstructor. and so on. You can mix and match these annotations as needed. But one thing to keep in mind is that they all must be inserted before the class declaration.

Note that the annotations are processed at compile time, so you will not see the generated code in your source files. But you can see the generated code in the compiled class files. You can also use your IDE to view the generated code. For example, in IntelliJ IDEA, you can browse the build directory and look at the generated files.

Be careful with @Data on JPA entities.

@Data generates getters, setters, equals(), hashCode(), and toString(). These generated methods are not always appropriate for persistent entities. Thus, we should not simply add @Data to every class.

In this course we will use Lombok selectively. For JPA entities, a common pattern will be:

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
Data Transfer Objects (DTOs) classes can use @Data when appropriate because DTOs generally have simpler lifecycle and equality requirements. We will talk more about DTOs in later activities.

Step 1 : Verify Lombok

In order to be able to use Lombok, you need to add it as a dependency to your project, in the build.gradle file (if you are using Gradle). Make sure you add the following dependency to your build.gradle file:

implementation 'org.projectlombok:lombok:1.18.20'
annotationProcessor 'org.projectlombok:lombok:1.18.20'
If your IDE reports errors for Lombok-generated methods, make sure annotation processing is enabled in your IDE. Also, the versions of Lombok and Spring Boot should be compatible. So you might need to change it to a different version of Lombok. Search for "Lombok version compatible with Spring Boot" to find the correct version.

Quick Check

Look at one of your classes that uses Lombok. Can you identify which methods are generated automatically?

Part 2: Turning an Entity into a JPA Entity

A DDD entity has an identity. A JPA entity also needs an identity so Hibernate can identify the corresponding database row.

Consider the Member aggregate root from Activity 02.

public class Member {
private UUID memberId;
private MemberName name;
private Email email;
private MembershipTier tier;
private MemberStatus status;
private int activeLoanCount;
private LocalDate registrationDate;
}

To make this class persistent, we add JPA annotations.

@Entity
@Table(name = "members")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Embedded
    private MemberId memberLibraryId;

    @Embedded
    private MemberName name;

    
    @Column(unique = true)
    @Embedded
    private EmailAddress email;

    @Min(value = 0, message = "Active loan count cannot be negative")
    private int activeLoanCount;

    @Enumerated(EnumType.STRING)
    private MemberStatus memberStatus;
    @Enumerated(EnumType.STRING)
    private MembershipTier membershipTier;
    private LocalDate registrationDate;

    // getters, setters are geneated by Lombok
    public Member(
            MemberId memberLibraryId,
            MemberName name,
            EmailAddress email,
            MembershipTier tier
    ) {
        this.memberLibraryId = memberLibraryId;
        this.name = name;
        this.email = email;
        this.membershipTier = tier;
        this.memberStatus = MemberStatus.ACTIVE;
        this.activeLoanCount = 0;
        this.registrationDate = LocalDate.now();
    }
}

Read the Code

Code Meaning
@Entity Marks the class as a JPA persistent entity.
@Table(name = "members") Specifies the database table name.
@Id Identifies the primary key.
@Embedded Embeds a Value Object into the entity table.
@Enumerated(EnumType.STRING) Stores the enum value as text.
@Getter Lombok generates getter methods.
@NoArgsConstructor(...) Provides the constructor required by JPA.
Notice: There is no @Setter on the entity. We do not want every field to become freely mutable (meaning we do not want to allow any field to be changed at any time by any part of the system). Domain behavior should control how important state changes. Meaning that the entity should provide methods that encapsulate the business rules for changing its state, rather than exposing setters for all fields.
Notice the constructor. The protected no-argument constructor exists for JPA. The public constructor is the constructor used by application code to create a valid Member.

in order to use these annotations you need to add the necessary dependency in build.gradle:

implementation 'org.projectlombok:lombok:1.18.20'
annotationProcessor 'org.projectlombok:lombok:1.18.20'
implementation 'jakarta.persistence:jakarta.persistence-api:3.2.0' implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

Part 3: Persisting Value Objects

In Activity 02, we identified concepts such as MemberId, MemberName, and EmailAddress and as Value Objects. see the diagram below. Value objects are immutable and do not have their own identity. thus, value objects should be implemented as immutable classes, meaning that their state cannot be changed after they are created. Here how we can implement the id and EmailAddress (We cannot use Email as the name of the class because it would conflict with @Email annotation from javax.validation.constraints package) value object as an immutable class:

DDD Diagram for Member Management Context
Figure A2.2 DDD Diagram for Member Management Context

Value Object implementation

Here is the code for the value objects: EmailAddress.


@Embeddable
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class EmailAddress {

    @Column(name = "email", nullable = false, unique = true)
    @Email
    private String value;

    // getters, setters are geneated by Lombok
}
    

The EmailAddress object does not need its own database table. Its value can be stored directly inside the members table (i.e. embedded inside that table). JPA provides a way to embed a Value Object inside an Entity using the @Embeddable annotation on the Value Object class and the @Embedded annotation on the Entity class.

@Embedded
private EmailAddress email;

Conceptually:

Member
│
├── memberId
├── name
├── email ────────────────┐
├── tier                  │
├── status                │
└── registrationDate      │
│
v
members table

member_id
first_name
last_name
email
tier
status
active_loan_count
registration_date 
Important Note: This only works if the is a one-to-one relationship between the entity and the value object. If the value object is shared by multiple entities, it should be stored in its own table and referenced by a foreign key. Alternatively, if the entity can have multiple value objects of the same type, the value object should be stored in its own table and referenced by a foreign key.

Here is the code for the value objects: MemberId and MemberName.


@Embeddable
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class MemberName {
    @NotBlank(message = "First name is required")
    private String firstName;
    @NotBlank(message = "Last name is required")
    private String lastName;
}  

@Embeddable
@Getter
@Setter
@AllArgsConstructor
public class MemberId {
    private UUID libraryId;
    public MemberId(){
        this.libraryId = UUID.randomUUID();
    }
} 

One could ask: what is the use of having the MemberId value object if it is just a wrapper around a UUID? The answer is that it provides a clear and explicit representation of the concept of a MemberId in the domain model. It allows us to encapsulate any validation or business rules related to MemberId in one place, and it makes the code more readable and expressive. For example, in library management context, the member ID is created based on some criteria/business rules set by the Library. This is the place where the MemberId value object shines. The code related to its creation and validation is centralized in one location.

Another question is why define id and memberId attributes in the Member entity? isn't that redundant? the answer is that the id attribute is used by JPA/Hibernate to identify the entity in the database (i.e. the primary key of the members table), while the memberId attribute is used by the domain model (which the library domain) to represent the concept of a member's unique identifier. One is an internal technical identifier used by the persistence layer, while the other is an external identifier used by the domain layer to represent a domain concept that has meaning in the business context.

Part 4 : Persisting Enumerations

Our domain model contains enumerations such as:

public enum MemberStatus {
ACTIVE,
SUSPENDED,
EXPIRED
 

}

JPA can store an enum in two ways:

@Enumerated(EnumType.ORDINAL)

or:

@Enumerated(EnumType.STRING)

We will use STRING.

@Enumerated(EnumType.STRING)
private MemberStatus status;

Why STRING?

Suppose the enum is:

ACTIVE,
SUSPENDED,
EXPIRED

With ORDINAL, the database might contain:

0
1
2

If the order of the enum changes later, existing database data can become incorrect.

With STRING, the database contains:

ACTIVE
SUSPENDED
EXPIRED
Course rule: For persistent enums, use @Enumerated(EnumType.STRING) unless you have a specific reason to do otherwise.

Part 5 : Repository Layers

DDD and Spring Data use the word repository in related but different ways. The domain layer should not need to know that we are using Spring Data JPA.

Domain Repository

Package: membermanagement.infrastructurelayer

public interface MemberRepository extends JpaRepository <Member, Integer>
{
}

This code defines the repository for the Member Entity. It exprends the JpaRepository and hence inherits the basic CRUD operations. Notice how we must specify the entity type (Member) and also the type of the primary key for that entity (Integer). This is done between the triangle brackets <Member, Integer>.

This interface expresses what the domain needs. It does not mention any specific database technology. With this you will be able to get basic CRUD operations.

Architecture: The domain knows about the repository abstraction. Infrastructure knows about Spring Data JPA.
Domain
|
v
Infrastructure
|
| MemberRepository
v
Spring Data JPA
|
v
Hibernate
|
v
H2 

Part 6 : Member Management context Complete Example

At this stage, we have implemented the Member Management context with all its components: entities, value objects, and enumerations. Let us test the implementation by running the application and checking the database schema generated by Hibernate. As exlained in the previous sections, the files should be placed under the domainlayer and infrastructurelayer packages of the membermanagement bounded context package. Also make sure to have the following enumerated types defined:

public enum MembershipTier {
STANDARD,
PREMIUM
}
public enum MemberStatus {
ACTIVE,
SUSPENDED,
EXPIRED
}

The following image shows the file structure of the membermanagement bounded context package.

Project Packages
Figure A3.2 Project Packages

Part 7: Set up H2 database

In order to use H2 database you need to add the necessary dependency:

// For H2 Database
runtimeOnly 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-h2console'

You also need to add configuration parameters in the application.properties file

# application.properties
spring.datasource.url=jdbc:h2:mem:librarydb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

# Enable the H2 web console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

Part 8 : Run the Application

At this point, you are ready to test the application using Intellij or ./gradlew bootrun command.

Watch the console carefully. Hibernate should report that it is creating database tables.

Part 9: Verify the Database Using H2 Console

This is the final step of Activity 03. We are not inserting any data yet. Instead, we want to verify that Hibernate has converted our Java model into a database schema.

Step 1 : Open H2 Console

Open:

http://localhost:8080/h2-console

Use the JDBC URL configured by your project. For example:

jdbc:h2:mem:librarydb

Use the username and password configured in application.properties or application.yml.

Your JDBC URL may be different. Do not blindly copy the example above. Check your project configuration.

Step 2 : Connect

Click Connect.

Step 3 : Inspect the Tables

Look at the list of tables generated by Hibernate.

You should see table "members" corresponding to your persistent entity we just created.

The exact table and column names depend on your JPA mappings and configuration.

Step 4 : Inspect MEMBERS

Open the MEMBERS table. Verify that columns corresponding to the entity and its embedded Value Objects exist.

For example:

LIBRARY_ID
FIRSTNAME
LAST_NAME
EMAIL
MEMBERSHIP_TIER
MEMBERSHIP_STATUS
ACTIVE_LOAN_COUNT
REGISTRATION_DATE

The following figure shows the expected h2-console interface.

H2-console
Figure A3.3 Expected H2-Console interface.

Part 10: Lombok and JPA: Important Pitfalls

Lombok is convenient, but convenience should not replace understanding.

Why not simply use @Data?

Consider:

@Entity
 

@Data
public class Member {
...
}

@Data generates several methods automatically. Some of these methods can be inappropriate for JPA entities.

  • Generated setters may allow uncontrolled state changes.
  • Generated equality may not match entity identity semantics.
  • toString() can become problematic when entities contain relationships.
  • Generated methods can interact poorly with lazy-loaded associations.

Therefore, in this course we use a more controlled pattern:

@Entity
 

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member {
...
}
Remember: Lombok should reduce boilerplate. It should not hide the design of your domain model.

Part 11 : Check Your Understanding

Before leaving the class, answer the following questions.

  1. What does @Entity tell Hibernate?
  2. What is the purpose of @Id?
  3. Why does Email use @Embeddable instead of @Entity?
  4. Why are we using @Enumerated(EnumType.STRING)?
  5. Why should a Loan contain borrowerId instead of a direct Member object?
  6. What is the main advantage of using Lombok?
  7. Why are we not simply using @Data on every JPA entity?

Troubleshooting

Problem: Lombok methods are not recognized

Check that Lombok is included in pom.xml and that annotation processing is enabled in your IDE.

Problem: Hibernate says there is no default constructor

JPA requires an accessible no-argument constructor. Check for:

@NoArgsConstructor(access = AccessLevel.PROTECTED)

Problem: H2 Console does not open

Make sure the Spring Boot application is running and that the H2 Console has been enabled in your configuration.

Problem: A table is missing

Check that the class has @Entity and that the package is included in Spring Boot's entity scanning.

Problem: Embedded fields have unexpected column names

Use @Column(name = "...") inside the Value Object when you need to control the database column name.

Problem: Hibernate reports duplicate column names

Two embedded Value Objects may contain fields that map to the same column name. Use explicit @Column names to resolve the collision.

Reflection

In this activity, we moved from:

DDD Model
 

↓
Java Classes
↓
JPA Mapping
↓
Hibernate
↓
Database Tables

Consider the following question:

Does JPA determine how your domain should be designed, or does JPA simply provide a way to persist the model you designed?

Another important question:

Lombok can eliminate hundreds of lines of boilerplate code. Should developers always use @Data? Explain why or why not, particularly for JPA entities.

Up Next : Activity 04

In Activity 03, we created the structure of our persistent domain model.

In Activity 04, we will work with actual persistent data.

 

Activity 03
Persist the Model
↓
Tables Created
↓
Activity 04
Work With Data
↓
Create
Read
Update
Delete
↓
Repositories 

We will learn how to use repositories to save and retrieve domain objects, initialize test data, and perform basic persistence operations.

Activity 03 complete: Your domain model now has a persistent representation. You have crossed the boundary from an in-memory object model to a relational database schema.

Appendix A : DDD to JPA Mapping

DDD Concept JPA Representation
Entity @Entity
Aggregate Root Usually a JPA @Entity
Identity @Id
Value Object @Embeddable
Embedded Value Object @Embedded
Enumeration @Enumerated(EnumType.STRING)
Domain Repository Domain-level repository interface
Persistence Repository JpaRepository
Inheritance @Inheritance
Inheritance discriminator @DiscriminatorColumn
Aggregate boundary Domain design; not automatically enforced by JPA
Final principle: JPA can map your objects. It cannot design your aggregates for you.

Appendix B : JPA Annotation Quick Reference

@Entity
    → Persistent class
 

@Table
→ Database table configuration

@Id
→ Primary key

@Embedded
→ Embed a Value Object

@Embeddable
→ Define a Value Object that can be embedded

@Column
→ Configure a database column

@Enumerated(EnumType.STRING)
→ Store enum names

@Inheritance
→ Configure entity inheritance

@DiscriminatorColumn
→ Identify subclass in SINGLE_TABLE inheritance

@DiscriminatorValue
→ Value identifying a particular subclass